feat(site): enforce VOICE.md's punctuation rule, and make it true - #486
Merged
Conversation
site/VOICE.md has said this since 2026-07-02:
Em-dashes are not used as sentence separators in site prose. Restructure
with a period, a comma, a colon, or parentheses instead.
Nothing enforced it. `_sloplib.in_antislop_doc_scope` governed repo-root docs
and `docs/**` and had never covered `site/`, so the rule was prose asking
politely - and 16 of the 36 authored pages violated it for 24 days while
reviewers cited it at contributors.
A rule with no gate is worse than no rule. It is not merely unenforced: people
learn that the style docs are advisory, which is the same corrosion as a
teardown report that over-counts its failures (#433) or a suite that is red once
in four (#462). So this ships the gate first and the prose second.
THE GATE. `.github/scripts/check_site_voice.py`, wired into docs.yml's
`site-check`, which `deploy` needs - so a violation blocks the publish. Its file
set comes from `git ls-files`, so a generated page cannot drift into scope by
landing in a tracked directory: if it is not committed, it is not audited. It
does not re-implement the exclusions; it asks the shared scope predicate, so
there is one definition of scope rather than two that can disagree.
SCOPE IS AUTHORED PROSE, and each exclusion is load-bearing rather than
convenient: `content/docs/reference/**` is generated on every build (91 of the
128 pages there), `changelog.md` is a verbatim pass-through of the repo
CHANGELOG under a different register, and `site/src/curated/**` mirrors
plugins/ca bodies that are already excluded via `plugins/`. A finding nobody
wrote and nobody can fix in place is noise, and noise is how a gate gets
ignored.
TWO DETECTOR CORRECTIONS, both found by writing the gate rather than by reading:
* The detector flagged DEFINITION-LIST dashes (`- **term** - meaning`). The
rule is about SENTENCE separators, and VOICE.md's own Terminology anchors
section is written in exactly that form - enforcing it as written would have
made the gate contradict the guide it enforces. Now exempt, anchored to
line-start and a bolded lead-in.
* That exemption's FIRST cut was a false-negative generator, caught by an
adversarial reader before it shipped. Blanking the whole `- **term**`
lead-in left the next dash with no word character on its left once inline
code was stripped, so on
- **The gate-enforcement hooks** - `a.py`, `b.py` - make zero calls.
a real separator scored 1 before and 0 after. It loosened the shared
detector for docs/** and repo-root too, not only for site prose. Only the
definition DASH is dropped now; the term is kept. The test that was supposed
to cover this passed only because its fixture had no inline code, so the
real line is now a fixture.
Also: the scope predicate keyed on `.md` alone, so the two authored `.mdx` pages
were invisible to a rule about writing.
THE PROSE. 118 offending lines across 15 pages, restructured by what each dash
was doing - parentheses or a comma pair for an aside, a colon for a consequence
or a list lead-in, a period where two independent statements had been welded.
Table cells are in scope (`| No - guarded |` became `| No: guarded |`): a reader
does not care that the dash sits in a cell. Verified: every code span, link
target, version, flag, path, gate ID and ADR reference is byte-identical across
all 15 files; no generated page, no curated mirror, and nothing outside
site/src/content/docs was touched; zero CRLF drift.
Two rewrites were revised after review rather than defended: an appositive that
wedged 16 words between subject and verb in install.md, and a fifth comma added
to a four-item list in enforcement.md - where pi.md already renders the
identical sentence with a period, so it now matches.
KNOWN GAP, recorded in the script and filed as #484 rather than implied to be
handled: the detector is line-based, so a separator dash at a line-wrap boundary
is invisible to it. Three such dashes were found BY HAND while fixing the
flagged ones. They are fixed, but the gate did not catch them and would not
catch a new one. Closing it needs paragraph-level analysis, which is a different
change from this gate.
Verified: gate exit 0 over 36 authored pages; site suite 438/438; sloplib 33
tests; full ca hook suite 1169; ci-impact 41; docs contract clean; sync-core
byte-identical across three plugins. ca-pi advances to 0.1.26 for the shared
hook change.
Closes #338.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
site/VOICE.mdhas said this since 2026-07-02:Nothing enforced it.
_sloplib.in_antislop_doc_scopegoverned repo-root docs anddocs/**and had never coveredsite/— so the rule was prose asking politely, and 16 of the 36 authored pages violated it for 24 days while reviewers cited it at contributors.A rule with no gate is worse than no rule: people learn the style docs are advisory. That is the same corrosion as a teardown report that over-counts its failures (#433) or a suite that is red once in four (#462). So this ships the gate first and the prose second.
The gate
.github/scripts/check_site_voice.py, wired intodocs.yml'ssite-check— whichdeployneeds, so a violation blocks the publish.Its file set comes from
git ls-files, so a generated page cannot drift into scope by landing in a tracked directory: if it isn't committed, it isn't audited. It doesn't re-implement the exclusions either; it asks the shared scope predicate, so there is one definition of scope rather than two that can disagree.Scope is authored prose, and each exclusion is load-bearing rather than convenient:
content/docs/reference/**content/docs/changelog.mdsite/src/curated/**plugins/cabodies, already excluded viaplugins/A finding nobody wrote and nobody can fix in place is noise, and noise is how a gate gets ignored.
Two detector corrections, both found by building the gate
The detector flagged definition-list dashes (
- **term** — meaning). The rule is about sentence separators, and VOICE.md's own Terminology anchors are written in exactly that form — enforcing it as written would have made the gate contradict the guide it enforces. Now exempt, anchored to line-start and a bolded lead-in.That exemption's first cut was a false-negative generator, caught by an adversarial reader before it shipped. Blanking the whole
- **term**lead-in left the next dash with no word character on its left once inline code was stripped, so on:a real separator scored 1 before and 0 after — and that loosened the shared detector for
docs/**and repo-root too, not only site prose. Only the definition dash is dropped now; the term is kept. The test meant to cover this passed only because its fixture had no inline code, so the real line is now a fixture.Also: the scope predicate keyed on
.mdalone, so the two authored.mdxpages were invisible to a rule about writing.The prose
118 offending lines across 15 pages, restructured by what each dash was doing — parentheses or a comma pair for an aside, a colon for a consequence or list lead-in, a period where two independent statements had been welded. Table cells are in scope (
| No — guarded |→| No: guarded |): a reader doesn't care that the dash sits in a cell.Verified: every code span, link target, version, flag, path, gate ID and ADR reference is byte-identical across all 15 files; no generated page, no curated mirror, and nothing outside
site/src/content/docswas touched; zero CRLF drift.Two rewrites were revised after review rather than defended: an appositive that wedged 16 words between subject and verb in
install.md, and a fifth comma added to a four-item list inenforcement.md— wherepi.mdalready renders the identical sentence with a period, so it now matches.Known gap, recorded rather than implied
The detector is line-based, so a separator dash at a line-wrap boundary is invisible to it. Three such dashes were found by hand while fixing the flagged ones. They're fixed, but the gate didn't catch them and wouldn't catch a new one. Closing it needs paragraph-level analysis — a different change from this gate. Documented in the script's docstring and filed as #484.
Verification
Gate exit 0 over 36 authored pages · site suite 438/438 · sloplib 33 tests · full ca hook suite 1169 ·
test_ci_impact41 · docs contract clean ·sync-core --checkbyte-identical across three plugins.ca-piadvances to 0.1.26 for the shared hook change.Closes #338.